﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Collections;
using System.Collections.Concurrent;
using System.Reflection;
using System.ComponentModel;
using System.Threading;
using System.IO;
using System.Xml.Serialization;
using System.Xml.Schema;

namespace TorrentProxy
{
    /// <summary>
    /// Base abstract class for settings
    /// </summary>    
    public abstract class BaseSettings
    {
        private object _syncObject = new object();
        private Boolean _isLocked;

        private readonly List<XmlNode> _notUsedNodes;
        private readonly ConcurrentDictionary<String, object> _settings;
        private readonly Dictionary<string, PropertyInfo> _properties;

        protected BaseSettings()
        {
            _notUsedNodes = new List<XmlNode>();
            _properties = new Dictionary<string, PropertyInfo>();
            _settings = new ConcurrentDictionary<string, object>();
            foreach (PropertyInfo propertyInfo in GetType().GetProperties())
            {
                var attr = (SettingItem)Attribute.GetCustomAttribute(propertyInfo, typeof(SettingItem));
                if (attr != null)
                {
                    _properties.Add(propertyInfo.Name, propertyInfo);
                    var defAttr = (DefaultValueAttribute)Attribute.GetCustomAttribute(propertyInfo, typeof(DefaultValueAttribute));
                    if (defAttr != null)
                    {
                        //Save values to internal dictionary
                        //propertyInfo.SetValue(this, defAttr.Value, null);
                        _settings[propertyInfo.Name] = defAttr.Value;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the setting by key
        /// </summary>
        public object this[String key]
        {
            get
            {
                /*
                PropertyInfo prop = (PropertyInfo)_properties[key];
                var attr = (SettingItem)Attribute.GetCustomAttribute(prop, typeof(SettingItem));
                if (attr == null)
                {
                    throw new ArgumentException(
                        String.Format(
                            "Property '{0}' cannot be used with BaseSettings attribute because it has no SettingItem attribute",
                            prop.Name));
                }

                if (!prop.CanRead)
                {
                    throw new ArgumentException(
                        String.Format(
                            "Property '{0}' cannot be used with BaseSettings attribute because it cannot be read",
                            prop.Name));
                }
                return prop.GetValue(this, null);
                */
                return _settings[key];
            }
            set
            {
                _settings[key] = value;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is locked.
        /// </summary>
        /// <value><c>true</c> if this instance is locked; otherwise, <c>false</c>.</value>
        protected Boolean IsLocked
        {
            get
            {
                return _isLocked;
            }
        }

        public void Load(String file)
        {
            XmlDocument xmlDoc = LoadXmlDocument(file);
            Load(xmlDoc);
        }

        public void Load(Stream stream)
        {
            XmlDocument xmlDoc = LoadXmlDocument(stream);
            Load(xmlDoc);
        }

        /// <summary>
        /// Loads settings.
        /// </summary>
        public void Load(XmlDocument xmlDoc)
        {
            Monitor.Enter(_syncObject, ref _isLocked);
            try
            {
                //Save values that are not used in this class to the array
                _notUsedNodes.Clear();
                foreach (XmlElement itemNode in xmlDoc.ChildNodes[0].ChildNodes)
                {
                    if (itemNode.HasAttribute("key"))
                    {
                        String key = itemNode.Attributes["key"].Value;

                        Boolean found = _properties.Keys.Contains(key);
                        /*
                        foreach (PropertyInfo propertyInfo in _properties.Values)
                        {
                            SettingItem attr = (SettingItem)Attribute.GetCustomAttribute(propertyInfo, typeof(SettingItem));
                            if (attr != null)
                            {
                                if (attr.Key.Equals(key))
                                {
                                    found = true;
                                    break;
                                }
                            }
                        }
                        */
                        if (!found)
                        {
                            _notUsedNodes.Add(itemNode);
                        }
                    }
                }

                //Load values to properties       
                foreach (PropertyInfo prop in _properties.Values)
                {
                    var attr = (SettingItem)Attribute.GetCustomAttribute(prop, typeof(SettingItem));

                    if (xmlDoc.DocumentElement == null)
                        continue;

                    var node = xmlDoc.DocumentElement.SelectSingleNode("descendant::item[@key='" + attr.Key + "']");
                    if (node != null)
                    {
                        if (((XmlElement)node).GetAttribute("IsNull") == "true")
                        {
                            //prop.SetValue(this, null, null);
                            _settings[prop.Name] = null;
                        }
                        else
                        {
                            Object val = DeserializeObject(prop.PropertyType, node.InnerXml, attr.DeserializerMethodName);
                            //prop.SetValue(this, val, null);
                            _settings[prop.Name] = val;
                        }
                    }
                    else
                    {
                        //check for default value
                        var defAttr = (DefaultValueAttribute)Attribute.GetCustomAttribute(prop, typeof(DefaultValueAttribute));
                        if (defAttr != null)
                        {
                            //prop.SetValue(this, defAttr.Value, null);
                            _settings[prop.Name] = defAttr.Value;
                        }
                    }
                }
            }
            finally
            {
                Monitor.Exit(_syncObject);
                _isLocked = false;
            }
        }

        /// <summary>
        /// Saves settings.
        /// </summary>
        public void Save(Stream stream)
        {
            Monitor.Enter(_syncObject, ref _isLocked);
            try
            {
                PrepareXmlDocForSave();
                CommitChanges(stream);
            }
            catch
            {
                DiscartChanges();
                throw;
            }
            finally
            {
                Monitor.Exit(_syncObject);
                _isLocked = false;
            }
        }

        /// <summary>
        /// Saves settings.
        /// </summary>
        public void Save(String file)
        {
            Monitor.Enter(_syncObject, ref _isLocked);
            try
            {
                PrepareXmlDocForSave();
                CommitChanges(file);
            }
            catch
            {
                DiscartChanges();
                throw;
            }
            finally
            {
                Monitor.Exit(_syncObject);
                _isLocked = false;
            }
        }

        private void PrepareXmlDocForSave()
        {
            //Save values that are not used in this class to the array
            _notUsedNodes.Clear();
            if (xmlDoc != null && xmlDoc.HasChildNodes)
            {
                foreach (XmlElement itemNode in xmlDoc.ChildNodes[0].ChildNodes)
                {
                    if (itemNode.HasAttribute("key"))
                    {
                        String key = itemNode.Attributes["key"].Value;

                        Boolean found = _properties.Keys.Contains(key);
                        /*
                        foreach (PropertyInfo propertyInfo in _properties.Values)
                        {
                            SettingItem attr = (SettingItem)Attribute.GetCustomAttribute(propertyInfo, typeof(SettingItem));
                            if (attr != null)
                            {
                                if (attr.Key.Equals(key))
                                {
                                    found = true;
                                    break;
                                }
                            }
                        }
                        */
                        if (!found)
                        {
                            _notUsedNodes.Add(itemNode);
                        }
                    }
                }
            }

            foreach (KeyValuePair<String, object> settings in _settings)
            {
                var attr = (SettingItem)Attribute.GetCustomAttribute(_properties[settings.Key], typeof(SettingItem));
                /*
                if (attr == null) continue;

                if (!prop.CanRead)
                {
                    throw new ArgumentException(
                        String.Format(
                            "Property '{0}' cannot be used with BaseSettings attribute because it cannot be read",
                            prop.Name));
                }
                var value = prop.GetValue(this, null);
                SaveItem(attr.Key, value, attr.SerializerMethodName);
                */
                SaveItem(settings.Key, settings.Value, attr.SerializerMethodName);
            }
            _notUsedNodes.ForEach(node => xmlDoc.ChildNodes[0].AppendChild(node));
        }

        private XmlDocument xmlDoc;

        private static XmlDocument LoadXmlDocument(String file)
        {
            XmlDocument xmlDocument = new XmlDocument();
            using (TextReader reader = new StreamReader(file))
            {
                xmlDocument.Load(reader);
            }
            return xmlDocument;
        }

        private static XmlDocument LoadXmlDocument(Stream stream)
        {
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.Load(stream);
            return xmlDocument;
        }

        private static void SaveXmlDocument(String file, XmlDocument xmlDocument)
        {
            xmlDocument.Save(file);
        }

        private static void SaveXmlDocument(Stream stream, XmlDocument xmlDocument)
        {
            xmlDocument.Save(stream);
        }

        /// <summary>
        /// Saves the item.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <param name="serializerMethodName">Name of the serializer method.</param>
        private void SaveItem(String key, Object value, String serializerMethodName)
        {
            XmlElement rootEl;
            if (xmlDoc == null)
            {
                xmlDoc = new XmlDocument();
                rootEl = xmlDoc.CreateElement("Settings");
                xmlDoc.AppendChild(rootEl);
                CreateAttribute(rootEl, "version", Assembly.GetExecutingAssembly().GetName().Version.ToString());
            }
            else
            {
                rootEl = xmlDoc.DocumentElement;
            }

            if (rootEl == null)
                return;

            XmlNode itemNode = xmlDoc.SelectSingleNode("descendant::item[@key='" + key + "']");
            //item is absent in the document
            if (itemNode == null)
            {
                itemNode = xmlDoc.CreateElement("item");
                rootEl.AppendChild(itemNode);
                CreateAttribute(itemNode, "key", key);
                String strValue = null;

                if (String.IsNullOrEmpty(serializerMethodName))
                {
                    strValue = SerializeObject(value);
                }
                else
                {
                    MethodInfo methodInfo = GetType().GetMethod(serializerMethodName,
                                                                BindingFlags.Static | BindingFlags.NonPublic);

                    if (methodInfo != null)
                    {
                        strValue = methodInfo.Invoke(this, new[] { value }).ToString();
                    }
                }

                if (strValue != null)
                {
                    itemNode.InnerXml = strValue;
                }
                else
                {
                    CreateAttribute(itemNode, "IsNull", "true");
                }
            }
            else
            {
                if (itemNode.Attributes[key] == null)
                {
                    CreateAttribute(itemNode, "key", key);
                }

                String strValue = null;

                if (String.IsNullOrEmpty(serializerMethodName))
                {
                    strValue = SerializeObject(value);
                }
                else
                {
                    MethodInfo methodInfo = GetType().GetMethod(serializerMethodName, BindingFlags.Static | BindingFlags.NonPublic);

                    if (methodInfo != null)
                    {
                        strValue = methodInfo.Invoke(this, new[] { value }).ToString();
                    }
                }

                if (strValue != null)
                {
                    ((XmlElement)itemNode).RemoveAttribute("IsNull");
                    itemNode.InnerXml = strValue;
                }
                else
                {
                    CreateAttribute(itemNode, "IsNull", "true");
                }
            }
        }

        /// <summary>
        /// Commits the changes.
        /// </summary>
        protected void CommitChanges(String file)
        {
            SaveXmlDocument(file, xmlDoc);
        }

        /// <summary>
        /// Commits the changes.
        /// </summary>
        protected void CommitChanges(Stream stream)
        {
            SaveXmlDocument(stream, xmlDoc);
        }

        /// <summary>
        /// Discarts the changes.
        /// </summary>
        protected void DiscartChanges()
        {
            xmlDoc = null;
            //Load(file);
        }

        /// <summary>
        /// Creates the xml attribute object.
        /// </summary>
        /// <param name="node">The node.</param>
        /// <param name="name">The attribute name.</param>
        /// <param name="value">The attribute value.</param>
        /// <returns></returns>
        private static void CreateAttribute(XmlNode node, String name, String value)
        {
            XmlAttribute attr = node.OwnerDocument.CreateAttribute(name);
            attr.Value = value;
            node.Attributes.Append(attr);
        }

        protected static String SerializeObject(Object item)
        {
            if (item == null)
                return null;

            try
            {
                var sb = new StringBuilder();
                var xws = new XmlWriterSettings
                {
                    OmitXmlDeclaration = true,
                    Encoding = Encoding.UTF8,
                    Indent = true,
                    CloseOutput = true
                };
                using (var xw = XmlWriter.Create(sb, xws))
                {
                    var xs = new XmlSerializer(item.GetType());
                    if (xw != null)
                    {
                        xs.Serialize(xw, item);
                    }
                    else
                    {
                        return String.Empty;
                    }
                }
                return sb.ToString();
            }
            catch (Exception ex)
            {
                return String.Empty;
            }
        }

        protected Object DeserializeObject(Type type, String xml, String deserializerMethodName)
        {
            if (String.IsNullOrEmpty(deserializerMethodName))
            {
                var xrs = new XmlReaderSettings
                {
                    IgnoreComments = true,
                    ValidationType = ValidationType.None,
                    ValidationFlags = XmlSchemaValidationFlags.None
                };

                using (var stream = new MemoryStream(StringToUTF8ByteArray(xml)))
                using (var xr = XmlReader.Create(stream, xrs))
                {
                    return new XmlSerializer(type).Deserialize(xr);
                }
            }

            MethodInfo methodInfo = GetType().GetMethod(deserializerMethodName, BindingFlags.Static | BindingFlags.NonPublic);

            if (methodInfo != null)
            {
                return methodInfo.Invoke(this, new[] { xml });
            }
            return null;
        }

        private static byte[] StringToUTF8ByteArray(String xml)
        {
            return new UTF8Encoding().GetBytes(xml);
        }

        [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
        internal protected sealed class SettingItem : Attribute
        {
            private readonly String key;
            private readonly String serializerMethodName;
            private readonly String deserializerMethodName;

            public String Key
            {
                get { return key; }
            }

            public String SerializerMethodName
            {
                get { return serializerMethodName; }
            }

            public String DeserializerMethodName
            {
                get { return deserializerMethodName; }
            }

            public SettingItem(String key)
            {
                this.key = key;
            }

            public SettingItem(String key, String serializerMethodName, String deserializerMethodName)
                : this(key)
            {
                this.serializerMethodName = serializerMethodName;
                this.deserializerMethodName = deserializerMethodName;
            }
        }
    }
}
